Variables

Datos

# Cargando datos
load("../data/my_train1.Rdata")
load("../data/my_test1.Rdata")
sampleSub <- read_csv("../data/sample_submission.csv")
head(new_train1)
  • Selecciono sólo las variables que van a ingresar al análisis.
library(tidyverse)

mi_train <- new_train1 %>% 
  select(-c(App, date_update))

mi_test <- new_test1 %>% 
  select(-c(App, date_update))

Exploración

  • El análisis exploratorio en este caso muestra evidencias de relaciones lineales y no lineales. La variable que podría ser de mayor importancia para clasificar el rating de las aplicaciones es el número de reseñas.
  • Los gráficos de dispersión muestran tendencias diferentes para aplicaciones exitosas y no exitosas.
  • Aplicar la transformación logarítmica podría ser de utilidad para mejorar la clasificación.
  • Tanto para análisis de componentes principales como para UMAP sólo uso las variables numéricas, no tuve en cuenta en este análisis las variables categóricas, aunque podrían haber ingresado al análisis como variables dummy.

Densidades Logaritmos

# Definiendo tema y colores para gráficos
theme_set(theme_bw())
colores <- c("#5B6DC8", "#D33B44")

mi_train %>% 
  select_if(is.numeric) %>% 
  select(-new_year) %>% 
  mutate(Rating = as.factor(Rating)) %>% 
  pivot_longer(cols = !Rating, names_to = "variable", values_to = "valores") %>% 
  ggplot(aes(x = valores, fill = Rating, color = Rating)) +
  facet_wrap(~variable, scales = "free", ncol = 3, nrow = 3) +
  geom_density(alpha = 0.4) +
  scale_color_manual(values = colores) +
  scale_fill_manual(values = colores) +
  scale_x_log10() +
  labs(title = "Escala logarítmica")

Correlaciones

  • Estas correlaciones las obtengo con las variables en escala logarítmica.
  • Correlación no paramétrica de Spearman.
library(corrr)
mi_train %>% 
  select_if(is.numeric) %>% 
  select(-new_year, -Rating) %>% 
  mutate_if(is.numeric, log) %>% 
  correlate(method = "spearman") %>% 
  rearrange() %>% 
  shave() %>% 
  rplot(print_cor = TRUE) +
  theme(axis.text.x = element_text(angle = 35, hjust = 1)) 

Dispersión

mi_train %>% 
  mutate(Rating = as.factor(Rating)) %>% 
  ggplot(aes(x = Reviews, y = new_installs, color = Rating)) +
  geom_point(size = 1) +
  scale_x_log10() +
  scale_y_log10() +
  geom_smooth(se = FALSE, size = 1) +
  scale_color_manual(values = colores) +
  labs(x = "Reseñas", y = "Descargas",
       title = "Descargas vs Reseñas",
       subtitle = "Escala logarítmica")

mi_train %>% 
  mutate(Rating = as.factor(Rating)) %>% 
  ggplot(aes(x = size_kb, y = Reviews, color = Rating)) +
  geom_point(size = 1, alpha = 0.5) +
  scale_x_log10() +
  scale_y_log10() +
  geom_smooth(se = FALSE, size = 1.5) +
  scale_color_manual(values = colores) +
  labs(x = "Tamaño de App", y = "Reseñas",
       title = "Reseñas vs Tamaño de App",
       subtitle = "Escala logarítmica")

mi_train %>% 
  mutate(Rating = as.factor(Rating)) %>% 
  ggplot(aes(x = new_version, y = Reviews, color = Rating)) +
  geom_point(size = 1, alpha = 0.5) +
  scale_x_log10() +
  scale_y_log10() +
  geom_smooth(se = FALSE, size = 1.5) +
  scale_color_manual(values = colores) +
  labs(x = "Reseñas", y = "Versión de App",
       title = "Reseñas vs Versión de App",
       subtitle = "Escala logarítmica")

mi_train %>% 
  mutate(Rating = as.factor(Rating)) %>% 
  ggplot(aes(x = new_subversion, y = Reviews, color = Rating)) +
  geom_point(size = 1, alpha = 0.5) +
  scale_x_log10() +
  scale_y_log10() +
  geom_smooth(se = FALSE, size = 1.5) +
  scale_color_manual(values = colores) +
  labs(x = "Reseñas", y = "Sub-versión de App",
       title = "Reseñas vs Versión de App",
       subtitle = "Escala logarítmica")

PCA

# "Receta" y "preparación" para PCA y UMAP
receta_reductDim <- mi_train %>%
  select_if(is.numeric) %>% 
  select(-new_year) %>% 
  recipe(~ .) %>% 
  update_role(Rating, new_role = "id") %>% 
  step_knnimpute(all_predictors()) %>% 
  step_BoxCox(all_predictors()) %>%
  step_normalize(all_predictors()) %>% 
  step_pca(all_predictors())
  
pca_prep <- receta_reductDim %>% prep()
pca_prep
Data Recipe

Inputs:

Training data contained 5788 data points and 2060 incomplete rows. 

Operations:

K-nearest neighbor imputation for new_day, size_kb, new_installs, new_price, new_month_num, ... [trained]
Box-Cox transformation on new_day, size_kb, new_month_num, min_android [trained]
Centering and scaling for Reviews, new_day, size_kb, new_installs, new_price, ... [trained]
PCA extraction with Reviews, new_day, size_kb, new_installs, new_price, ... [trained]
  • Recuperando resultados de PCA:
# Cargas (loadings)
tidy_pca <- tidy(pca_prep, 4) 
tidy_pca %>% 
  filter(component %in% paste0("PC", 1:5)) %>% 
  ggplot(aes(x = value, y = terms, fill = value)) +
  facet_wrap(~component, ncol = 5) +
  geom_col(show.legend = FALSE)

  • Gráfico conn nuevas coordenadas:
juice(pca_prep) %>% 
  ggplot(aes(x = PC1, y = PC2, color = as.factor(Rating))) + 
  geom_point() +
  geom_vline(xintercept = 0, lty = 2, lwd = 0.1) +
  geom_hline(yintercept = 0, lty = 2, lwd = 0.1) +
  scale_color_manual(values = colores)

  • Gráfico de 3 primeras componentes:
library(plotly)
plot_ly(x = ~ PC1, y = ~ PC2, z = ~PC3, data = juice(pca_prep),
        color = ~as.factor(Rating)) %>% 
  add_markers()
minimal value for n is 3, returning requested palette with 3 different levels
minimal value for n is 3, returning requested palette with 3 different levels
minimal value for n is 3, returning requested palette with 3 different levels
minimal value for n is 3, returning requested palette with 3 different levels

UMAP

library(embed)
# "Receta" y "preparación"  UMAP
receta_umap <- mi_train %>%
  select_if(is.numeric) %>% 
  select(-new_year) %>% 
  recipe(~ .) %>% 
  update_role(Rating, new_role = "id") %>% 
  step_knnimpute(all_predictors()) %>% 
  step_BoxCox(all_predictors()) %>%
  step_normalize(all_predictors()) %>% 
  step_umap(all_predictors())
  
umap_prep <- receta_umap %>% prep()
umap_prep
Data Recipe

Inputs:

Training data contained 5788 data points and 2060 incomplete rows. 

Operations:

K-nearest neighbor imputation for new_day, size_kb, new_installs, ... [trained]
Box-Cox transformation on new_day, size_kb, new_month_num, min_android [trained]
Centering and scaling for Reviews, new_day, size_kb, new_installs, ... [trained]
UMAP embedding for Reviews, new_day, size_kb, new_installs, ... [trained]
  • Gráfico con nuevas coordenadas:
juice(umap_prep) %>% 
  ggplot(aes(x = umap_1, y = umap_2, color = as.factor(Rating))) + 
  geom_point() +
  geom_vline(xintercept = 0, lty = 2, lwd = 0.1) +
  geom_hline(yintercept = 0, lty = 2, lwd = 0.1) +
  scale_color_manual(values = colores)

¿Respuesta Imbalanceada?

mi_train %>% 
  mutate(Rating = as.factor(Rating)) %>% 
  count(Rating) %>% 
  ggplot(aes(x = Rating, y = n, fill = Rating, color = Rating,
             label = n)) +
  geom_col() +
  geom_label(color = "white") +
  scale_color_manual(values = colores) +
  scale_fill_manual(values = colores) +
  theme(legend.position = "none")

Modelos

  • Utilizo la meta-biblioteca tidymodels que contiene bibliotecas específicas para construcción de modelos de machine learning.
  • En este caso ajusto modelos lineales generalizos con regularización (Regresión Ridge, Regresión Lasso y ElasticNet)
  • Preprocesamiento e ingeniería de características:
    • Se prueban modelos con imputación a través del método de k-vecinos más cercanos.
    • Las variables se estandarizan para la construcción de modelos.
    • Pruebo modelos con y sin escala logarítmica.
    • Pruebo modelos con y sin transformación de Box-Cox para normalizar las variables.
    • Etiquetas de baja frecuencia les agrego el nivel “otro”.
library(tidymodels)
  • La siguiente imagen (tomada de tidymodels.org) denota la estrategia de evaluación de modelos que adopto.

Preprocesamiento

  • Fracciono los datos en train y test con proporciones de 80 y 20%, respectivamente. Estratifico la partición en función de la variable que indica si la app tiene algún costo.
  • Pruebo el sigueinte preprocesamiento:
    • Receta: estandarización, binarización, datos con imputación por knn, transformación de Box-Cox, asignación de etiqueta “otra” a niveles de baja frecuencia y como es un problema de clasificación imbalanceado, utilizo la biblioteca themis para generar clases balanceadas a través de muestreo con reemplazo.

Receta Train - Validación

library(themis)
# Train-Test
set.seed(2020)
data_split <- initial_split(data = mi_train, prop = 0.80, strata = Type)
data_train <- training(data_split) %>% mutate(Rating = as.factor(Rating))
data_test <- testing(data_split) %>% mutate(Rating = as.factor(Rating))

# Receta
receta1 <- recipe(Rating ~ ., data = data_train) %>% 
  step_knnimpute(all_predictors(), neighbors = 5) %>%
  step_BoxCox(all_numeric(), -all_outcomes()) %>% 
  step_other(all_nominal(), -all_outcomes(), other = "otra") %>% 
  step_dummy(all_nominal(), -all_outcomes(), one_hot = TRUE) %>%  
  step_normalize(all_numeric(), -all_outcomes()) %>% 
  step_upsample(Rating)

receta1_prep <- receta1 %>% 
  prep()

Submission

receta_sub <- recipe(~ ., data = mi_test) %>% 
  step_knnimpute(all_predictors(), neighbors = 5) %>%
  step_BoxCox(all_numeric(), -all_outcomes()) %>% 
  step_other(all_nominal(), -all_outcomes(), other = "otra") %>% 
  step_dummy(all_nominal(), -all_outcomes(), one_hot = TRUE) %>%  
  step_normalize(all_numeric(), -all_outcomes())

prep_sub <- prep(receta_sub)
data_sub <- juice(prep_sub)

Ajuste

Lasso Inicial

lasso1 <- logistic_reg(penalty = 0.1, mixture = 1) %>% 
  set_mode("classification") %>% 
  set_engine("glmnet")

wf1 <- workflow() %>% 
  add_recipe(receta1)

res_lasso1 <- wf1 %>% 
  add_model(lasso1) %>% 
  fit(data = data_train)

res_lasso1 %>% 
  tidy()

Tuning

  • Bootstrapping estratificado con 10 submuestras como estrategia para evaluar el modelo.
  • Ajuste de hiperparámetros “penalty” y “mixture”.
set.seed(2020)
data_boost <- bootstraps(data_train, strata = Type, times = 10)

tune_lasso <- logistic_reg(penalty = tune(), mixture = tune()) %>% 
  set_mode("classification") %>% 
  set_engine("glmnet")

mi_grid <- grid_regular(penalty(),
                        mixture(),
                        levels = 50)

doParallel::registerDoParallel()
set.seed(1992)
lasso_grid <- tune_grid(
  wf1 %>% add_model(tune_lasso),
  resamples = data_boost,
  grid = mi_grid
)

doParallel::stopImplicitCluster()
  • Métricas de error (total de filas: 5 mil):
lasso_grid  %>% 
  collect_metrics() %>% 
  head()
  • Gráfico de métricas de error: hiperparámetros penalty y mixture.
lasso_grid  %>% 
  collect_metrics() %>%  
  ggplot(aes(x = penalty, y = mean, color = .metric)) +
  geom_line() +
  geom_smooth(se = FALSE) +
  facet_wrap(~.metric, scales = "free", nrow = 2) +
  scale_x_log10() +
  scale_color_manual(values = colores) +
  theme(legend.position = "none")

lasso_grid  %>% 
  collect_metrics() %>%  
  ggplot(aes(x = mixture, y = mean, color = .metric)) +
  geom_line() +
  geom_smooth(se = FALSE) +
  facet_wrap(~.metric, scales = "free", nrow = 2) +
  scale_x_log10() +
  scale_color_manual(values = colores) +
  theme(legend.position = "none")

Mejor modelo

mejor_tuning <- lasso_grid %>% 
  select_best(metric = "roc_auc")
mejor_tuning

Resultado Final

  • Flujo final con ajuste del modelo con mejores hiperparámetros.
  • Este modelo es el que se usa para predecir el rating del “test” en la competencia.
  • Obtengo la importancia de variables con la biblitoeca vip.

Importancia de variables

library(vip)
# Flujo final
modelo_final <- finalize_workflow(wf1 %>% add_model(tune_lasso),
                                  parameters = mejor_tuning)
# Ajuste final
modelo_final %>% 
  fit(data_train) %>% 
  pull_workflow_fit() %>% 
  vi(lambda = mejor_tuning$penalty) %>% 
  mutate(Importance = abs(Importance),
         Variable = fct_reorder(Variable, Importance)) %>% 
  ggplot(aes(x = Importance, y = Variable, color = Sign, fill = Sign)) +
  geom_col(alpha = 0.7) +
  scale_color_manual(values = colores) +
  scale_fill_manual(values = colores) +
  scale_x_continuous(expand = c(0, 0)) +
  labs(title = "Importancia de variables")

Modelo Final

ajuste_final <- modelo_final %>% 
  fit(data_train)
ajuste_final
== Workflow [trained] ====================================================================================
Preprocessor: Recipe
Model: logistic_reg()

-- Preprocessor ------------------------------------------------------------------------------------------
6 Recipe Steps

* step_knnimpute()
* step_BoxCox()
* step_other()
* step_dummy()
* step_normalize()
* step_upsample()

-- Model -------------------------------------------------------------------------------------------------

Call:  glmnet::glmnet(x = as.matrix(x), y = y, family = "binomial",      alpha = ~1) 

   Df %Dev   Lambda
1   0 0.00 0.066880
2   1 0.22 0.060940
3   1 0.40 0.055520
4   1 0.55 0.050590
5   1 0.68 0.046100
6   1 0.79 0.042000
7   1 0.87 0.038270
8   1 0.95 0.034870
9   6 1.07 0.031770
10  6 1.29 0.028950
11  7 1.47 0.026380
12  8 1.65 0.024030
13  8 1.81 0.021900
14  9 1.96 0.019950
15 10 2.11 0.018180
16 11 2.25 0.016570
17 13 2.39 0.015090
18 13 2.52 0.013750
19 13 2.64 0.012530
20 14 2.74 0.011420
21 14 2.84 0.010400
22 15 2.92 0.009480
23 15 3.00 0.008638
24 15 3.07 0.007870
25 15 3.12 0.007171
26 16 3.17 0.006534
27 16 3.22 0.005954
28 16 3.25 0.005425
29 16 3.29 0.004943
30 17 3.31 0.004504
31 17 3.34 0.004104
32 17 3.36 0.003739
33 17 3.38 0.003407
34 18 3.39 0.003104
35 18 3.41 0.002828
36 18 3.42 0.002577
37 18 3.43 0.002348
38 18 3.44 0.002140
39 18 3.45 0.001950
40 18 3.46 0.001776
41 18 3.46 0.001619
42 18 3.47 0.001475
43 18 3.47 0.001344
44 18 3.48 0.001224
45 18 3.48 0.001116
46 19 3.48 0.001016

...
and 6 more lines.
  • Estimativas (parámetros) del modelo final:
tidy(ajuste_final)

Predicciones

  • La misma receta inicial se aplica sobre el conjunto de validación, añadiendo la transformación.
test_baked  <- bake(object = receta1_prep, new_data = data_test)
head(test_baked)

Train

predichos_train <- ajuste_final$fit$fit %>%
  predict(new_data = juice(receta1_prep) %>% select(-Rating), type = "class") %>%
  bind_cols(juice(receta1_prep) %>%  select(Rating)) %>% 
  mutate_all(as.factor)
head(predichos_train)

Matriz de confusión

predichos_train %>%
  conf_mat(Rating, .pred_class) %>%
  pluck(1) %>%
  as_tibble() %>%
  ggplot(aes(x = Prediction, y = Truth, alpha = n)) +
  geom_tile(show.legend = FALSE) +
  geom_text(aes(label = n), colour = "white", alpha = 1, size = 8)

  • Precisión en train:
predichos_train %>%
  metrics(Rating, .pred_class) %>%
  select(-.estimator) %>%
  filter(.metric == "accuracy") 
  • F1-Score:
predichos_train%>%
  f_meas(Rating, .pred_class) %>%
  select(-.estimator) 
  • Curva ROC:
ajuste_final$fit$fit %>%
  predict(new_data = juice(receta1_prep) %>% select(-Rating), type = "prob") %>%  
  bind_cols(juice(receta1_prep) %>%  select(Rating)) %>% 
  roc_curve(Rating, .pred_0) %>% 
  autoplot()+
  labs(title = "ROC - Train")

Validación

predichos_test <- ajuste_final$fit$fit %>%
  predict(new_data = test_baked %>% select(-Rating), type = "class") %>%
  bind_cols(test_baked %>% select(Rating)) %>% 
  mutate_all(as.factor)
head(predichos_test)

Matriz de confusión

predichos_test %>%
  conf_mat(Rating, .pred_class) %>%
  pluck(1) %>%
  as_tibble() %>%
  ggplot(aes(x = Prediction, y = Truth, alpha = n)) +
  geom_tile(show.legend = FALSE) +
  geom_text(aes(label = n), colour = "white", alpha = 1, size = 8)

  • Precisión en test:
predichos_test %>%
  metrics(Rating, .pred_class) %>%
  select(-.estimator) %>%
  filter(.metric == "accuracy") 
  • F1-Score:
predichos_test %>%
  f_meas(Rating, .pred_class) %>%
  select(-.estimator) 
  • Curva ROC:
ajuste_final$fit$fit %>%
  predict(new_data = test_baked %>% select(-Rating), type = "prob") %>%
  bind_cols(test_baked %>% select(Rating)) %>%
  roc_curve(Rating, .pred_0) %>% 
  autoplot() +
  labs(title = "ROC - Test")

Submission

#predicciones
predichos_final1 <- ajuste_final$fit$fit %>%
  predict(new_data = data_sub, type = "class")

# Submission
sampleSub %>% 
  select(-rating) %>% 
  mutate(rating = predichos_final1$.pred_class) ->
  sub_02_glmnet
head(sub_02_glmnet)
  • Exportando predicciones:
write_csv(sub_02_glmnet, file = "../submission/glmnet_02.csv")
LS0tDQp0aXRsZTogIlByZWRpY2Npw7NuIGRlbCByYXRpbmcgZGUgTGFzIGFwbGljYWNpb25lcyBlbiBHb29nbGUgUGxheSBTdG9yZSINCnN1YnRpdGxlOiAiUmV0byBEYXRhU291cmNlIg0KYXV0aG9yOiAiW0VkaW1lciAoU2lkZXJldXMpXShodHRwczovL2VkaW1lci5naXRodWIuaW8vKSINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICB0b2M6IHRydWUNCiAgICB0b2NfZmxvYXQ6IA0KICAgICAgc21vb3RoX3Njcm9sbDogZmFsc2UNCiAgICAgIGNvbGxhcHNlZDogZmFsc2UNCiAgICBoaWdobGlnaHQ6IHB5Z21lbnRzDQogICAgdGhlbWU6IHNwYWNlbGFiDQogICAgY3NzOiBlc3RpbG8uY3NzDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQotLS0NCg0KPGNlbnRlcj4NCjxpbWcgc3JjID0gIi4uL2ltZy9jb21wZXRlbmNpYS5wbmciIC8+DQo8L2NlbnRlcj4NCg0KYGBge3IsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgd2FybmluZyA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgIG1lc3NhZ2UgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICBmaWcuYWxpZ24gPSAiY2VudGVyIikNCmBgYA0KDQotIFtTaXRpbyBvZmljaWFsIGRlbCByZXRvIGVuIERhdGFTb3VyY2UuXShodHRwczovL3d3dy5kYXRhc291cmNlLmFpL2VzL2hvbWUvZGF0YS1zY2llbmNlLWNvbXBldGl0aW9ucy1mb3Itc3RhcnR1cHMvcHJlZGljaWVuZG8tZWwtcmF0aW5nLWRlLWxhcy1hcGxpY2FjaW9uZXMtZW4tZ29vZ2xlLXBsYXktc3RvcmUpDQoNCiMgVmFyaWFibGVzDQoNCjxjZW50ZXI+DQo8aW1nIHNyYyA9ICIuLi9pbWcvdmFyaWFibGVzLlBORyIgLz4NCjwvY2VudGVyPg0KDQojIERhdG9zDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KIyBDYXJnYW5kbyBkYXRvcw0KbG9hZCgiLi4vZGF0YS9teV90cmFpbjEuUmRhdGEiKQ0KbG9hZCgiLi4vZGF0YS9teV90ZXN0MS5SZGF0YSIpDQpzYW1wbGVTdWIgPC0gcmVhZF9jc3YoIi4uL2RhdGEvc2FtcGxlX3N1Ym1pc3Npb24uY3N2IikNCmhlYWQobmV3X3RyYWluMSkNCmBgYA0KDQotIFNlbGVjY2lvbm8gc8OzbG8gbGFzIHZhcmlhYmxlcyBxdWUgdmFuIGEgaW5ncmVzYXIgYWwgYW7DoWxpc2lzLg0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KDQptaV90cmFpbiA8LSBuZXdfdHJhaW4xICU+JSANCiAgc2VsZWN0KC1jKEFwcCwgZGF0ZV91cGRhdGUpKQ0KDQptaV90ZXN0IDwtIG5ld190ZXN0MSAlPiUgDQogIHNlbGVjdCgtYyhBcHAsIGRhdGVfdXBkYXRlKSkNCmBgYA0KDQojIEV4cGxvcmFjacOzbiB7LnRhYnNldCAudGFic2V0LWZhZGUgLnRhYnNldC1waWxsc30NCg0KLSBFbCBhbsOhbGlzaXMgZXhwbG9yYXRvcmlvIGVuIGVzdGUgY2FzbyBtdWVzdHJhIGV2aWRlbmNpYXMgZGUgcmVsYWNpb25lcyBsaW5lYWxlcyB5IG5vIGxpbmVhbGVzLiBMYSB2YXJpYWJsZSBxdWUgcG9kcsOtYSBzZXIgZGUgbWF5b3IgaW1wb3J0YW5jaWEgcGFyYSBjbGFzaWZpY2FyIGVsIHJhdGluZyBkZSBsYXMgYXBsaWNhY2lvbmVzIGVzIGVsIG7Dum1lcm8gZGUgcmVzZcOxYXMuDQotIExvcyBncsOhZmljb3MgZGUgZGlzcGVyc2nDs24gbXVlc3RyYW4gdGVuZGVuY2lhcyBkaWZlcmVudGVzIHBhcmEgYXBsaWNhY2lvbmVzIGV4aXRvc2FzIHkgbm8gZXhpdG9zYXMuDQotIEFwbGljYXIgbGEgdHJhbnNmb3JtYWNpw7NuIGxvZ2Fyw610bWljYSBwb2Ryw61hIHNlciBkZSB1dGlsaWRhZCBwYXJhIG1lam9yYXIgbGEgY2xhc2lmaWNhY2nDs24uDQotIFRhbnRvIHBhcmEgW2Fuw6FsaXNpcyBkZSBjb21wb25lbnRlcyBwcmluY2lwYWxlc10oKSBjb21vIHBhcmEgW1VNQVBdKGh0dHBzOi8vYXJ4aXYub3JnL2Ficy8xODAyLjAzNDI2KSBzw7NsbyB1c28gbGFzIHZhcmlhYmxlcyBudW3DqXJpY2FzLCBubyB0dXZlIGVuIGN1ZW50YSBlbiBlc3RlIGFuw6FsaXNpcyBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcywgYXVucXVlIHBvZHLDrWFuIGhhYmVyIGluZ3Jlc2FkbyBhbCBhbsOhbGlzaXMgY29tbyB2YXJpYWJsZXMgZHVtbXkuDQoNCiMjIERlbnNpZGFkZXMgTG9nYXJpdG1vcw0KDQpgYGB7ciwgZmlnLndpZHRoPTl9DQojIERlZmluaWVuZG8gdGVtYSB5IGNvbG9yZXMgcGFyYSBncsOhZmljb3MNCnRoZW1lX3NldCh0aGVtZV9idygpKQ0KY29sb3JlcyA8LSBjKCIjNUI2REM4IiwgIiNEMzNCNDQiKQ0KDQptaV90cmFpbiAlPiUgDQogIHNlbGVjdF9pZihpcy5udW1lcmljKSAlPiUgDQogIHNlbGVjdCgtbmV3X3llYXIpICU+JSANCiAgbXV0YXRlKFJhdGluZyA9IGFzLmZhY3RvcihSYXRpbmcpKSAlPiUgDQogIHBpdm90X2xvbmdlcihjb2xzID0gIVJhdGluZywgbmFtZXNfdG8gPSAidmFyaWFibGUiLCB2YWx1ZXNfdG8gPSAidmFsb3JlcyIpICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gdmFsb3JlcywgZmlsbCA9IFJhdGluZywgY29sb3IgPSBSYXRpbmcpKSArDQogIGZhY2V0X3dyYXAofnZhcmlhYmxlLCBzY2FsZXMgPSAiZnJlZSIsIG5jb2wgPSAzLCBucm93ID0gMykgKw0KICBnZW9tX2RlbnNpdHkoYWxwaGEgPSAwLjQpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGNvbG9yZXMpICsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gY29sb3JlcykgKw0KICBzY2FsZV94X2xvZzEwKCkgKw0KICBsYWJzKHRpdGxlID0gIkVzY2FsYSBsb2dhcsOtdG1pY2EiKQ0KYGBgDQoNCiMjIENvcnJlbGFjaW9uZXMNCg0KLSBFc3RhcyBjb3JyZWxhY2lvbmVzIGxhcyBvYnRlbmdvIGNvbiBsYXMgdmFyaWFibGVzIGVuIGVzY2FsYSBsb2dhcsOtdG1pY2EuDQotIENvcnJlbGFjacOzbiBubyBwYXJhbcOpdHJpY2EgZGUgU3BlYXJtYW4uDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KbGlicmFyeShjb3JycikNCm1pX3RyYWluICU+JSANCiAgc2VsZWN0X2lmKGlzLm51bWVyaWMpICU+JSANCiAgc2VsZWN0KC1uZXdfeWVhciwgLVJhdGluZykgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgbG9nKSAlPiUgDQogIGNvcnJlbGF0ZShtZXRob2QgPSAic3BlYXJtYW4iKSAlPiUgDQogIHJlYXJyYW5nZSgpICU+JSANCiAgc2hhdmUoKSAlPiUgDQogIHJwbG90KHByaW50X2NvciA9IFRSVUUpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAzNSwgaGp1c3QgPSAxKSkgDQpgYGANCg0KIyMgRGlzcGVyc2nDs24NCg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCm1pX3RyYWluICU+JSANCiAgbXV0YXRlKFJhdGluZyA9IGFzLmZhY3RvcihSYXRpbmcpKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IFJldmlld3MsIHkgPSBuZXdfaW5zdGFsbHMsIGNvbG9yID0gUmF0aW5nKSkgKw0KICBnZW9tX3BvaW50KHNpemUgPSAxKSArDQogIHNjYWxlX3hfbG9nMTAoKSArDQogIHNjYWxlX3lfbG9nMTAoKSArDQogIGdlb21fc21vb3RoKHNlID0gRkFMU0UsIHNpemUgPSAxKSArDQogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjb2xvcmVzKSArDQogIGxhYnMoeCA9ICJSZXNlw7FhcyIsIHkgPSAiRGVzY2FyZ2FzIiwNCiAgICAgICB0aXRsZSA9ICJEZXNjYXJnYXMgdnMgUmVzZcOxYXMiLA0KICAgICAgIHN1YnRpdGxlID0gIkVzY2FsYSBsb2dhcsOtdG1pY2EiKQ0KYGBgDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbWlfdHJhaW4gJT4lIA0KICBtdXRhdGUoUmF0aW5nID0gYXMuZmFjdG9yKFJhdGluZykpICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gc2l6ZV9rYiwgeSA9IFJldmlld3MsIGNvbG9yID0gUmF0aW5nKSkgKw0KICBnZW9tX3BvaW50KHNpemUgPSAxLCBhbHBoYSA9IDAuNSkgKw0KICBzY2FsZV94X2xvZzEwKCkgKw0KICBzY2FsZV95X2xvZzEwKCkgKw0KICBnZW9tX3Ntb290aChzZSA9IEZBTFNFLCBzaXplID0gMS41KSArDQogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjb2xvcmVzKSArDQogIGxhYnMoeCA9ICJUYW1hw7FvIGRlIEFwcCIsIHkgPSAiUmVzZcOxYXMiLA0KICAgICAgIHRpdGxlID0gIlJlc2XDsWFzIHZzIFRhbWHDsW8gZGUgQXBwIiwNCiAgICAgICBzdWJ0aXRsZSA9ICJFc2NhbGEgbG9nYXLDrXRtaWNhIikNCmBgYA0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCm1pX3RyYWluICU+JSANCiAgbXV0YXRlKFJhdGluZyA9IGFzLmZhY3RvcihSYXRpbmcpKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IG5ld192ZXJzaW9uLCB5ID0gUmV2aWV3cywgY29sb3IgPSBSYXRpbmcpKSArDQogIGdlb21fcG9pbnQoc2l6ZSA9IDEsIGFscGhhID0gMC41KSArDQogIHNjYWxlX3hfbG9nMTAoKSArDQogIHNjYWxlX3lfbG9nMTAoKSArDQogIGdlb21fc21vb3RoKHNlID0gRkFMU0UsIHNpemUgPSAxLjUpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGNvbG9yZXMpICsNCiAgbGFicyh4ID0gIlJlc2XDsWFzIiwgeSA9ICJWZXJzacOzbiBkZSBBcHAiLA0KICAgICAgIHRpdGxlID0gIlJlc2XDsWFzIHZzIFZlcnNpw7NuIGRlIEFwcCIsDQogICAgICAgc3VidGl0bGUgPSAiRXNjYWxhIGxvZ2Fyw610bWljYSIpDQpgYGANCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQptaV90cmFpbiAlPiUgDQogIG11dGF0ZShSYXRpbmcgPSBhcy5mYWN0b3IoUmF0aW5nKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBuZXdfc3VidmVyc2lvbiwgeSA9IFJldmlld3MsIGNvbG9yID0gUmF0aW5nKSkgKw0KICBnZW9tX3BvaW50KHNpemUgPSAxLCBhbHBoYSA9IDAuNSkgKw0KICBzY2FsZV94X2xvZzEwKCkgKw0KICBzY2FsZV95X2xvZzEwKCkgKw0KICBnZW9tX3Ntb290aChzZSA9IEZBTFNFLCBzaXplID0gMS41KSArDQogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjb2xvcmVzKSArDQogIGxhYnMoeCA9ICJSZXNlw7FhcyIsIHkgPSAiU3ViLXZlcnNpw7NuIGRlIEFwcCIsDQogICAgICAgdGl0bGUgPSAiUmVzZcOxYXMgdnMgVmVyc2nDs24gZGUgQXBwIiwNCiAgICAgICBzdWJ0aXRsZSA9ICJFc2NhbGEgbG9nYXLDrXRtaWNhIikNCmBgYA0KDQojIyBQQ0ENCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQojICJSZWNldGEiIHkgInByZXBhcmFjacOzbiIgcGFyYSBQQ0EgeSBVTUFQDQpyZWNldGFfcmVkdWN0RGltIDwtIG1pX3RyYWluICU+JQ0KICBzZWxlY3RfaWYoaXMubnVtZXJpYykgJT4lIA0KICBzZWxlY3QoLW5ld195ZWFyKSAlPiUgDQogIHJlY2lwZSh+IC4pICU+JSANCiAgdXBkYXRlX3JvbGUoUmF0aW5nLCBuZXdfcm9sZSA9ICJpZCIpICU+JSANCiAgc3RlcF9rbm5pbXB1dGUoYWxsX3ByZWRpY3RvcnMoKSkgJT4lIA0KICBzdGVwX0JveENveChhbGxfcHJlZGljdG9ycygpKSAlPiUNCiAgc3RlcF9ub3JtYWxpemUoYWxsX3ByZWRpY3RvcnMoKSkgJT4lIA0KICBzdGVwX3BjYShhbGxfcHJlZGljdG9ycygpKQ0KICANCnBjYV9wcmVwIDwtIHJlY2V0YV9yZWR1Y3REaW0gJT4lIHByZXAoKQ0KcGNhX3ByZXANCmBgYA0KDQotICoqUmVjdXBlcmFuZG8gcmVzdWx0YWRvcyBkZSBQQ0E6KioNCg0KYGBge3IsIGZpZy53aWR0aD05LCBmaWcuaGVpZ2h0PTN9DQojIENhcmdhcyAobG9hZGluZ3MpDQp0aWR5X3BjYSA8LSB0aWR5KHBjYV9wcmVwLCA0KSANCnRpZHlfcGNhICU+JSANCiAgZmlsdGVyKGNvbXBvbmVudCAlaW4lIHBhc3RlMCgiUEMiLCAxOjUpKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IHZhbHVlLCB5ID0gdGVybXMsIGZpbGwgPSB2YWx1ZSkpICsNCiAgZmFjZXRfd3JhcCh+Y29tcG9uZW50LCBuY29sID0gNSkgKw0KICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKQ0KYGBgDQoNCi0gKipHcsOhZmljbyBjb25uIG51ZXZhcyBjb29yZGVuYWRhczoqKg0KDQpgYGB7cn0NCmp1aWNlKHBjYV9wcmVwKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IFBDMSwgeSA9IFBDMiwgY29sb3IgPSBhcy5mYWN0b3IoUmF0aW5nKSkpICsgDQogIGdlb21fcG9pbnQoKSArDQogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDAsIGx0eSA9IDIsIGx3ZCA9IDAuMSkgKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLCBsdHkgPSAyLCBsd2QgPSAwLjEpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGNvbG9yZXMpDQpgYGANCg0KLSAqKkdyw6FmaWNvIGRlIDMgcHJpbWVyYXMgY29tcG9uZW50ZXM6KioNCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHBsb3RseSkNCnBsb3RfbHkoeCA9IH4gUEMxLCB5ID0gfiBQQzIsIHogPSB+UEMzLCBkYXRhID0ganVpY2UocGNhX3ByZXApLA0KICAgICAgICBjb2xvciA9IH5hcy5mYWN0b3IoUmF0aW5nKSkgJT4lIA0KICBhZGRfbWFya2VycygpDQpgYGANCg0KIyMgVU1BUA0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoZW1iZWQpDQojICJSZWNldGEiIHkgInByZXBhcmFjacOzbiIgIFVNQVANCnJlY2V0YV91bWFwIDwtIG1pX3RyYWluICU+JQ0KICBzZWxlY3RfaWYoaXMubnVtZXJpYykgJT4lIA0KICBzZWxlY3QoLW5ld195ZWFyKSAlPiUgDQogIHJlY2lwZSh+IC4pICU+JSANCiAgdXBkYXRlX3JvbGUoUmF0aW5nLCBuZXdfcm9sZSA9ICJpZCIpICU+JSANCiAgc3RlcF9rbm5pbXB1dGUoYWxsX3ByZWRpY3RvcnMoKSkgJT4lIA0KICBzdGVwX0JveENveChhbGxfcHJlZGljdG9ycygpKSAlPiUNCiAgc3RlcF9ub3JtYWxpemUoYWxsX3ByZWRpY3RvcnMoKSkgJT4lIA0KICBzdGVwX3VtYXAoYWxsX3ByZWRpY3RvcnMoKSkNCiAgDQp1bWFwX3ByZXAgPC0gcmVjZXRhX3VtYXAgJT4lIHByZXAoKQ0KdW1hcF9wcmVwDQpgYGANCg0KLSAqKkdyw6FmaWNvIGNvbiBudWV2YXMgY29vcmRlbmFkYXM6KioNCg0KYGBge3J9DQpqdWljZSh1bWFwX3ByZXApICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gdW1hcF8xLCB5ID0gdW1hcF8yLCBjb2xvciA9IGFzLmZhY3RvcihSYXRpbmcpKSkgKyANCiAgZ2VvbV9wb2ludCgpICsNCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gMCwgbHR5ID0gMiwgbHdkID0gMC4xKSArDQogIGdlb21faGxpbmUoeWludGVyY2VwdCA9IDAsIGx0eSA9IDIsIGx3ZCA9IDAuMSkgKw0KICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gY29sb3JlcykNCmBgYA0KDQoNCiMjIMK/UmVzcHVlc3RhIEltYmFsYW5jZWFkYT8NCg0KYGBge3J9DQptaV90cmFpbiAlPiUgDQogIG11dGF0ZShSYXRpbmcgPSBhcy5mYWN0b3IoUmF0aW5nKSkgJT4lIA0KICBjb3VudChSYXRpbmcpICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gUmF0aW5nLCB5ID0gbiwgZmlsbCA9IFJhdGluZywgY29sb3IgPSBSYXRpbmcsDQogICAgICAgICAgICAgbGFiZWwgPSBuKSkgKw0KICBnZW9tX2NvbCgpICsNCiAgZ2VvbV9sYWJlbChjb2xvciA9ICJ3aGl0ZSIpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGNvbG9yZXMpICsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gY29sb3JlcykgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpDQpgYGANCg0KDQojIE1vZGVsb3MgDQoNCi0gVXRpbGl6byBsYSBtZXRhLWJpYmxpb3RlY2EgW3RpZHltb2RlbHNdKGh0dHBzOi8vd3d3LnRpZHltb2RlbHMub3JnKSBxdWUgY29udGllbmUgYmlibGlvdGVjYXMgZXNwZWPDrWZpY2FzIHBhcmEgY29uc3RydWNjacOzbiBkZSBtb2RlbG9zIGRlIG1hY2hpbmUgbGVhcm5pbmcuDQotIEVuIGVzdGUgY2FzbyBhanVzdG8gW21vZGVsb3MgbGluZWFsZXMgZ2VuZXJhbGl6b3MgY29uIHJlZ3VsYXJpemFjacOzbl0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvUmVndWxhcml6ZWRfbGVhc3Rfc3F1YXJlcyM6fjp0ZXh0PVJlZ3VsYXJpemVkJTIwbGVhc3QlMjBzcXVhcmVzJTIwKFJMUyklMjBpcyxleGNlZWRzJTIwdGhlJTIwbnVtYmVyJTIwb2YlMjBvYnNlcnZhdGlvbnMuKSAoW1JlZ3Jlc2nDs24gUmlkZ2VdKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL1Rpa2hvbm92X3JlZ3VsYXJpemF0aW9uKSwgW1JlZ3Jlc2nDs24gTGFzc29dKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0xhc3NvXyhzdGF0aXN0aWNzKSkgeSBbRWxhc3RpY05ldF0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvRWxhc3RpY19uZXRfcmVndWxhcml6YXRpb24pKQ0KLSAqKlByZXByb2Nlc2FtaWVudG8gZSBpbmdlbmllcsOtYSBkZSBjYXJhY3RlcsOtc3RpY2FzOioqDQogIC0gU2UgcHJ1ZWJhbiBtb2RlbG9zIGNvbiBpbXB1dGFjacOzbiBhIHRyYXbDqXMgZGVsIG3DqXRvZG8gZGUgay12ZWNpbm9zIG3DoXMgY2VyY2Fub3MuDQogIC0gTGFzIHZhcmlhYmxlcyBzZSBlc3RhbmRhcml6YW4gcGFyYSBsYSBjb25zdHJ1Y2Npw7NuIGRlIG1vZGVsb3MuDQogIC0gUHJ1ZWJvIG1vZGVsb3MgY29uIHkgc2luIGVzY2FsYSBsb2dhcsOtdG1pY2EuDQogIC0gUHJ1ZWJvIG1vZGVsb3MgY29uIHkgc2luIFt0cmFuc2Zvcm1hY2nDs24gZGUgQm94LUNveF0oaHR0cHM6Ly9lcy53aWtpcGVkaWEub3JnL3dpa2kvVHJhbnNmb3JtYWNpJUMzJUIzbl9Cb3gtQ294KSBwYXJhIG5vcm1hbGl6YXIgbGFzIHZhcmlhYmxlcy4NCiAgLSBFdGlxdWV0YXMgZGUgYmFqYSBmcmVjdWVuY2lhIGxlcyBhZ3JlZ28gZWwgbml2ZWwgIm90cm8iLg0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeW1vZGVscykNCmBgYA0KDQotIExhIHNpZ3VpZW50ZSBpbWFnZW4gKHRvbWFkYSBkZSBbdGlkeW1vZGVscy5vcmddKGh0dHBzOi8vd3d3LnRpZHltb2RlbHMub3JnL3N0YXJ0L2Nhc2Utc3R1ZHkvKSkgZGVub3RhIGxhIGVzdHJhdGVnaWEgZGUgZXZhbHVhY2nDs24gZGUgbW9kZWxvcyBxdWUgYWRvcHRvLg0KDQo8Y2VudGVyPg0KPGltZyBzcmMgPSAiaHR0cHM6Ly93d3cudGlkeW1vZGVscy5vcmcvc3RhcnQvY2FzZS1zdHVkeS9pbWcvdmFsaWRhdGlvbi1zcGxpdC5zdmciIC8+DQo8L2NlbnRlcj4NCg0KIyMgUHJlcHJvY2VzYW1pZW50byB7LnRhYnNldCAudGFic2V0LWZhZGUgLnRhYnNldC1waWxsc30NCg0KLSBGcmFjY2lvbm8gbG9zIGRhdG9zIGVuIHRyYWluIHkgdGVzdCBjb24gcHJvcG9yY2lvbmVzIGRlIDgwIHkgMjAlLCByZXNwZWN0aXZhbWVudGUuIEVzdHJhdGlmaWNvIGxhIHBhcnRpY2nDs24gZW4gZnVuY2nDs24gZGUgbGEgdmFyaWFibGUgcXVlIGluZGljYSBzaSBsYSBhcHAgdGllbmUgYWxnw7puIGNvc3RvLg0KLSBQcnVlYm8gZWwgc2lndWVpbnRlIHByZXByb2Nlc2FtaWVudG86DQogIC0gKipSZWNldGE6KiogZXN0YW5kYXJpemFjacOzbiwgYmluYXJpemFjacOzbiwgZGF0b3MgY29uIGltcHV0YWNpw7NuIHBvciBrbm4sIHRyYW5zZm9ybWFjacOzbiBkZSBCb3gtQ294LCBhc2lnbmFjacOzbiBkZSBldGlxdWV0YSAib3RyYSIgYSBuaXZlbGVzIGRlIGJhamEgZnJlY3VlbmNpYSB5IGNvbW8gZXMgdW4gcHJvYmxlbWEgZGUgY2xhc2lmaWNhY2nDs24gaW1iYWxhbmNlYWRvLCB1dGlsaXpvIGxhIGJpYmxpb3RlY2EgW3RoZW1pc10oaHR0cHM6Ly9naXRodWIuY29tL3RpZHltb2RlbHMvdGhlbWlzKSBwYXJhIGdlbmVyYXIgY2xhc2VzIGJhbGFuY2VhZGFzIGEgdHJhdsOpcyBkZSBtdWVzdHJlbyBjb24gcmVlbXBsYXpvLg0KDQojIyMgUmVjZXRhIFRyYWluIC0gVmFsaWRhY2nDs24NCg0KYGBge3J9DQpsaWJyYXJ5KHRoZW1pcykNCiMgVHJhaW4tVGVzdA0Kc2V0LnNlZWQoMjAyMCkNCmRhdGFfc3BsaXQgPC0gaW5pdGlhbF9zcGxpdChkYXRhID0gbWlfdHJhaW4sIHByb3AgPSAwLjgwLCBzdHJhdGEgPSBUeXBlKQ0KZGF0YV90cmFpbiA8LSB0cmFpbmluZyhkYXRhX3NwbGl0KSAlPiUgbXV0YXRlKFJhdGluZyA9IGFzLmZhY3RvcihSYXRpbmcpKQ0KZGF0YV90ZXN0IDwtIHRlc3RpbmcoZGF0YV9zcGxpdCkgJT4lIG11dGF0ZShSYXRpbmcgPSBhcy5mYWN0b3IoUmF0aW5nKSkNCg0KIyBSZWNldGENCnJlY2V0YTEgPC0gcmVjaXBlKFJhdGluZyB+IC4sIGRhdGEgPSBkYXRhX3RyYWluKSAlPiUgDQogIHN0ZXBfa25uaW1wdXRlKGFsbF9wcmVkaWN0b3JzKCksIG5laWdoYm9ycyA9IDUpICU+JQ0KICBzdGVwX0JveENveChhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JSANCiAgc3RlcF9vdGhlcihhbGxfbm9taW5hbCgpLCAtYWxsX291dGNvbWVzKCksIG90aGVyID0gIm90cmEiKSAlPiUgDQogIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLWFsbF9vdXRjb21lcygpLCBvbmVfaG90ID0gVFJVRSkgJT4lICANCiAgc3RlcF9ub3JtYWxpemUoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUgDQogIHN0ZXBfdXBzYW1wbGUoUmF0aW5nKQ0KDQpyZWNldGExX3ByZXAgPC0gcmVjZXRhMSAlPiUgDQogIHByZXAoKQ0KYGBgDQoNCg0KIyMjIFN1Ym1pc3Npb24NCg0KYGBge3J9DQpyZWNldGFfc3ViIDwtIHJlY2lwZSh+IC4sIGRhdGEgPSBtaV90ZXN0KSAlPiUgDQogIHN0ZXBfa25uaW1wdXRlKGFsbF9wcmVkaWN0b3JzKCksIG5laWdoYm9ycyA9IDUpICU+JQ0KICBzdGVwX0JveENveChhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JSANCiAgc3RlcF9vdGhlcihhbGxfbm9taW5hbCgpLCAtYWxsX291dGNvbWVzKCksIG90aGVyID0gIm90cmEiKSAlPiUgDQogIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLWFsbF9vdXRjb21lcygpLCBvbmVfaG90ID0gVFJVRSkgJT4lICANCiAgc3RlcF9ub3JtYWxpemUoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKQ0KDQpwcmVwX3N1YiA8LSBwcmVwKHJlY2V0YV9zdWIpDQpkYXRhX3N1YiA8LSBqdWljZShwcmVwX3N1YikNCmBgYA0KDQojIyBBanVzdGUNCg0KIyMjIExhc3NvIEluaWNpYWwNCg0KYGBge3J9DQpsYXNzbzEgPC0gbG9naXN0aWNfcmVnKHBlbmFsdHkgPSAwLjEsIG1peHR1cmUgPSAxKSAlPiUgDQogIHNldF9tb2RlKCJjbGFzc2lmaWNhdGlvbiIpICU+JSANCiAgc2V0X2VuZ2luZSgiZ2xtbmV0IikNCg0Kd2YxIDwtIHdvcmtmbG93KCkgJT4lIA0KICBhZGRfcmVjaXBlKHJlY2V0YTEpDQoNCnJlc19sYXNzbzEgPC0gd2YxICU+JSANCiAgYWRkX21vZGVsKGxhc3NvMSkgJT4lIA0KICBmaXQoZGF0YSA9IGRhdGFfdHJhaW4pDQoNCnJlc19sYXNzbzEgJT4lIA0KICB0aWR5KCkNCmBgYA0KDQojIyMgVHVuaW5nIA0KDQotIEJvb3RzdHJhcHBpbmcgZXN0cmF0aWZpY2FkbyBjb24gMTAgc3VibXVlc3RyYXMgY29tbyBlc3RyYXRlZ2lhIHBhcmEgZXZhbHVhciBlbCBtb2RlbG8uDQotIEFqdXN0ZSBkZSBoaXBlcnBhcsOhbWV0cm9zICJwZW5hbHR5IiB5ICJtaXh0dXJlIi4NCg0KYGBge3J9DQpzZXQuc2VlZCgyMDIwKQ0KZGF0YV9ib29zdCA8LSBib290c3RyYXBzKGRhdGFfdHJhaW4sIHN0cmF0YSA9IFR5cGUsIHRpbWVzID0gMTApDQoNCnR1bmVfbGFzc28gPC0gbG9naXN0aWNfcmVnKHBlbmFsdHkgPSB0dW5lKCksIG1peHR1cmUgPSB0dW5lKCkpICU+JSANCiAgc2V0X21vZGUoImNsYXNzaWZpY2F0aW9uIikgJT4lIA0KICBzZXRfZW5naW5lKCJnbG1uZXQiKQ0KDQptaV9ncmlkIDwtIGdyaWRfcmVndWxhcihwZW5hbHR5KCksDQogICAgICAgICAgICAgICAgICAgICAgICBtaXh0dXJlKCksDQogICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHMgPSA1MCkNCg0KZG9QYXJhbGxlbDo6cmVnaXN0ZXJEb1BhcmFsbGVsKCkNCnNldC5zZWVkKDE5OTIpDQpsYXNzb19ncmlkIDwtIHR1bmVfZ3JpZCgNCiAgd2YxICU+JSBhZGRfbW9kZWwodHVuZV9sYXNzbyksDQogIHJlc2FtcGxlcyA9IGRhdGFfYm9vc3QsDQogIGdyaWQgPSBtaV9ncmlkDQopDQoNCmRvUGFyYWxsZWw6OnN0b3BJbXBsaWNpdENsdXN0ZXIoKQ0KYGBgDQoNCi0gKipNw6l0cmljYXMgZGUgZXJyb3IgKHRvdGFsIGRlIGZpbGFzOiA1IG1pbCk6KioNCg0KYGBge3J9DQpsYXNzb19ncmlkICAlPiUgDQogIGNvbGxlY3RfbWV0cmljcygpICU+JSANCiAgaGVhZCgpDQpgYGANCg0KLSAqKkdyw6FmaWNvIGRlIG3DqXRyaWNhcyBkZSBlcnJvcjoqKiBoaXBlcnBhcsOhbWV0cm9zICpwZW5hbHR5KiB5ICptaXh0dXJlKi4NCg0KYGBge3J9DQpsYXNzb19ncmlkICAlPiUgDQogIGNvbGxlY3RfbWV0cmljcygpICU+JSAgDQogIGdncGxvdChhZXMoeCA9IHBlbmFsdHksIHkgPSBtZWFuLCBjb2xvciA9IC5tZXRyaWMpKSArDQogIGdlb21fbGluZSgpICsNCiAgZ2VvbV9zbW9vdGgoc2UgPSBGQUxTRSkgKw0KICBmYWNldF93cmFwKH4ubWV0cmljLCBzY2FsZXMgPSAiZnJlZSIsIG5yb3cgPSAyKSArDQogIHNjYWxlX3hfbG9nMTAoKSArDQogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjb2xvcmVzKSArDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikNCmBgYA0KDQpgYGB7cn0NCmxhc3NvX2dyaWQgICU+JSANCiAgY29sbGVjdF9tZXRyaWNzKCkgJT4lICANCiAgZ2dwbG90KGFlcyh4ID0gbWl4dHVyZSwgeSA9IG1lYW4sIGNvbG9yID0gLm1ldHJpYykpICsNCiAgZ2VvbV9saW5lKCkgKw0KICBnZW9tX3Ntb290aChzZSA9IEZBTFNFKSArDQogIGZhY2V0X3dyYXAofi5tZXRyaWMsIHNjYWxlcyA9ICJmcmVlIiwgbnJvdyA9IDIpICsNCiAgc2NhbGVfeF9sb2cxMCgpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGNvbG9yZXMpICsNCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQ0KYGBgDQojIyMgTWVqb3IgbW9kZWxvDQoNCmBgYHtyfQ0KbWVqb3JfdHVuaW5nIDwtIGxhc3NvX2dyaWQgJT4lIA0KICBzZWxlY3RfYmVzdChtZXRyaWMgPSAicm9jX2F1YyIpDQptZWpvcl90dW5pbmcNCmBgYA0KDQojIyBSZXN1bHRhZG8gRmluYWwNCg0KLSBGbHVqbyBmaW5hbCBjb24gYWp1c3RlIGRlbCBtb2RlbG8gY29uIG1lam9yZXMgaGlwZXJwYXLDoW1ldHJvcy4NCi0gRXN0ZSBtb2RlbG8gZXMgZWwgcXVlIHNlIHVzYSBwYXJhIHByZWRlY2lyIGVsIHJhdGluZyBkZWwgInRlc3QiIGVuIGxhIGNvbXBldGVuY2lhLg0KLSBPYnRlbmdvIGxhIGltcG9ydGFuY2lhIGRlIHZhcmlhYmxlcyBjb24gbGEgYmlibGl0b2VjYSAqdmlwKi4NCg0KIyMjIEltcG9ydGFuY2lhIGRlIHZhcmlhYmxlcw0KDQpgYGB7ciwgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTl9DQpsaWJyYXJ5KHZpcCkNCiMgRmx1am8gZmluYWwNCm1vZGVsb19maW5hbCA8LSBmaW5hbGl6ZV93b3JrZmxvdyh3ZjEgJT4lIGFkZF9tb2RlbCh0dW5lX2xhc3NvKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbWV0ZXJzID0gbWVqb3JfdHVuaW5nKQ0KIyBBanVzdGUgZmluYWwNCm1vZGVsb19maW5hbCAlPiUgDQogIGZpdChkYXRhX3RyYWluKSAlPiUgDQogIHB1bGxfd29ya2Zsb3dfZml0KCkgJT4lIA0KICB2aShsYW1iZGEgPSBtZWpvcl90dW5pbmckcGVuYWx0eSkgJT4lIA0KICBtdXRhdGUoSW1wb3J0YW5jZSA9IGFicyhJbXBvcnRhbmNlKSwNCiAgICAgICAgIFZhcmlhYmxlID0gZmN0X3Jlb3JkZXIoVmFyaWFibGUsIEltcG9ydGFuY2UpKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IEltcG9ydGFuY2UsIHkgPSBWYXJpYWJsZSwgY29sb3IgPSBTaWduLCBmaWxsID0gU2lnbikpICsNCiAgZ2VvbV9jb2woYWxwaGEgPSAwLjcpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGNvbG9yZXMpICsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gY29sb3JlcykgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMoZXhwYW5kID0gYygwLCAwKSkgKw0KICBsYWJzKHRpdGxlID0gIkltcG9ydGFuY2lhIGRlIHZhcmlhYmxlcyIpDQpgYGANCg0KIyMjIE1vZGVsbyBGaW5hbA0KDQpgYGB7cn0NCmFqdXN0ZV9maW5hbCA8LSBtb2RlbG9fZmluYWwgJT4lIA0KICBmaXQoZGF0YV90cmFpbikNCmFqdXN0ZV9maW5hbA0KYGBgDQoNCi0gKipFc3RpbWF0aXZhcyAocGFyw6FtZXRyb3MpIGRlbCBtb2RlbG8gZmluYWw6KioNCg0KYGBge3J9DQp0aWR5KGFqdXN0ZV9maW5hbCkNCmBgYA0KDQojIFByZWRpY2Npb25lcw0KDQotIExhIG1pc21hIHJlY2V0YSBpbmljaWFsIHNlIGFwbGljYSBzb2JyZSBlbCBjb25qdW50byBkZSB2YWxpZGFjacOzbiwgYcOxYWRpZW5kbyBsYSB0cmFuc2Zvcm1hY2nDs24uDQoNCmBgYHtyfQ0KdGVzdF9iYWtlZCAgPC0gYmFrZShvYmplY3QgPSByZWNldGExX3ByZXAsIG5ld19kYXRhID0gZGF0YV90ZXN0KQ0KaGVhZCh0ZXN0X2Jha2VkKQ0KYGBgDQoNCiMjIFRyYWluDQoNCmBgYHtyfQ0KcHJlZGljaG9zX3RyYWluIDwtIGFqdXN0ZV9maW5hbCRmaXQkZml0ICU+JQ0KICBwcmVkaWN0KG5ld19kYXRhID0ganVpY2UocmVjZXRhMV9wcmVwKSAlPiUgc2VsZWN0KC1SYXRpbmcpLCB0eXBlID0gImNsYXNzIikgJT4lDQogIGJpbmRfY29scyhqdWljZShyZWNldGExX3ByZXApICU+JSAgc2VsZWN0KFJhdGluZykpICU+JSANCiAgbXV0YXRlX2FsbChhcy5mYWN0b3IpDQpoZWFkKHByZWRpY2hvc190cmFpbikNCmBgYA0KDQojIyMgTWF0cml6IGRlIGNvbmZ1c2nDs24NCg0KYGBge3J9DQpwcmVkaWNob3NfdHJhaW4gJT4lDQogIGNvbmZfbWF0KFJhdGluZywgLnByZWRfY2xhc3MpICU+JQ0KICBwbHVjaygxKSAlPiUNCiAgYXNfdGliYmxlKCkgJT4lDQogIGdncGxvdChhZXMoeCA9IFByZWRpY3Rpb24sIHkgPSBUcnV0aCwgYWxwaGEgPSBuKSkgKw0KICBnZW9tX3RpbGUoc2hvdy5sZWdlbmQgPSBGQUxTRSkgKw0KICBnZW9tX3RleHQoYWVzKGxhYmVsID0gbiksIGNvbG91ciA9ICJ3aGl0ZSIsIGFscGhhID0gMSwgc2l6ZSA9IDgpDQpgYGANCg0KLSAqKlByZWNpc2nDs24gZW4gdHJhaW46KioNCg0KYGBge3J9DQpwcmVkaWNob3NfdHJhaW4gJT4lDQogIG1ldHJpY3MoUmF0aW5nLCAucHJlZF9jbGFzcykgJT4lDQogIHNlbGVjdCgtLmVzdGltYXRvcikgJT4lDQogIGZpbHRlcigubWV0cmljID09ICJhY2N1cmFjeSIpIA0KYGBgDQoNCi0gKipGMS1TY29yZToqKg0KDQpgYGB7cn0NCnByZWRpY2hvc190cmFpbiU+JQ0KICBmX21lYXMoUmF0aW5nLCAucHJlZF9jbGFzcykgJT4lDQogIHNlbGVjdCgtLmVzdGltYXRvcikgDQpgYGANCg0KLSAqKkN1cnZhIFJPQzoqKg0KDQpgYGB7cn0NCmFqdXN0ZV9maW5hbCRmaXQkZml0ICU+JQ0KICBwcmVkaWN0KG5ld19kYXRhID0ganVpY2UocmVjZXRhMV9wcmVwKSAlPiUgc2VsZWN0KC1SYXRpbmcpLCB0eXBlID0gInByb2IiKSAlPiUgIA0KICBiaW5kX2NvbHMoanVpY2UocmVjZXRhMV9wcmVwKSAlPiUgIHNlbGVjdChSYXRpbmcpKSAlPiUgDQogIHJvY19jdXJ2ZShSYXRpbmcsIC5wcmVkXzApICU+JSANCiAgYXV0b3Bsb3QoKSsNCiAgbGFicyh0aXRsZSA9ICJST0MgLSBUcmFpbiIpDQpgYGANCg0KDQojIyBWYWxpZGFjacOzbg0KDQpgYGB7cn0NCnByZWRpY2hvc190ZXN0IDwtIGFqdXN0ZV9maW5hbCRmaXQkZml0ICU+JQ0KICBwcmVkaWN0KG5ld19kYXRhID0gdGVzdF9iYWtlZCAlPiUgc2VsZWN0KC1SYXRpbmcpLCB0eXBlID0gImNsYXNzIikgJT4lDQogIGJpbmRfY29scyh0ZXN0X2Jha2VkICU+JSBzZWxlY3QoUmF0aW5nKSkgJT4lIA0KICBtdXRhdGVfYWxsKGFzLmZhY3RvcikNCmhlYWQocHJlZGljaG9zX3Rlc3QpDQpgYGANCg0KIyMjIE1hdHJpeiBkZSBjb25mdXNpw7NuDQoNCmBgYHtyfQ0KcHJlZGljaG9zX3Rlc3QgJT4lDQogIGNvbmZfbWF0KFJhdGluZywgLnByZWRfY2xhc3MpICU+JQ0KICBwbHVjaygxKSAlPiUNCiAgYXNfdGliYmxlKCkgJT4lDQogIGdncGxvdChhZXMoeCA9IFByZWRpY3Rpb24sIHkgPSBUcnV0aCwgYWxwaGEgPSBuKSkgKw0KICBnZW9tX3RpbGUoc2hvdy5sZWdlbmQgPSBGQUxTRSkgKw0KICBnZW9tX3RleHQoYWVzKGxhYmVsID0gbiksIGNvbG91ciA9ICJ3aGl0ZSIsIGFscGhhID0gMSwgc2l6ZSA9IDgpDQpgYGANCg0KLSAqKlByZWNpc2nDs24gZW4gdGVzdDoqKg0KDQpgYGB7cn0NCnByZWRpY2hvc190ZXN0ICU+JQ0KICBtZXRyaWNzKFJhdGluZywgLnByZWRfY2xhc3MpICU+JQ0KICBzZWxlY3QoLS5lc3RpbWF0b3IpICU+JQ0KICBmaWx0ZXIoLm1ldHJpYyA9PSAiYWNjdXJhY3kiKSANCmBgYA0KDQotICoqRjEtU2NvcmU6KioNCg0KYGBge3J9DQpwcmVkaWNob3NfdGVzdCAlPiUNCiAgZl9tZWFzKFJhdGluZywgLnByZWRfY2xhc3MpICU+JQ0KICBzZWxlY3QoLS5lc3RpbWF0b3IpIA0KYGBgDQoNCi0gKipDdXJ2YSBST0M6KioNCg0KYGBge3J9DQphanVzdGVfZmluYWwkZml0JGZpdCAlPiUNCiAgcHJlZGljdChuZXdfZGF0YSA9IHRlc3RfYmFrZWQgJT4lIHNlbGVjdCgtUmF0aW5nKSwgdHlwZSA9ICJwcm9iIikgJT4lDQogIGJpbmRfY29scyh0ZXN0X2Jha2VkICU+JSBzZWxlY3QoUmF0aW5nKSkgJT4lDQogIHJvY19jdXJ2ZShSYXRpbmcsIC5wcmVkXzApICU+JSANCiAgYXV0b3Bsb3QoKSArDQogIGxhYnModGl0bGUgPSAiUk9DIC0gVGVzdCIpDQpgYGANCg0KIyBTdWJtaXNzaW9uDQoNCmBgYHtyfQ0KI3ByZWRpY2Npb25lcw0KcHJlZGljaG9zX2ZpbmFsMSA8LSBhanVzdGVfZmluYWwkZml0JGZpdCAlPiUNCiAgcHJlZGljdChuZXdfZGF0YSA9IGRhdGFfc3ViLCB0eXBlID0gImNsYXNzIikNCg0KIyBTdWJtaXNzaW9uDQpzYW1wbGVTdWIgJT4lIA0KICBzZWxlY3QoLXJhdGluZykgJT4lIA0KICBtdXRhdGUocmF0aW5nID0gcHJlZGljaG9zX2ZpbmFsMSQucHJlZF9jbGFzcykgLT4NCiAgc3ViXzAyX2dsbW5ldA0KaGVhZChzdWJfMDJfZ2xtbmV0KQ0KYGBgDQoNCi0gKipFeHBvcnRhbmRvIHByZWRpY2Npb25lczoqKg0KDQpgYGB7cn0NCndyaXRlX2NzdihzdWJfMDJfZ2xtbmV0LCBmaWxlID0gIi4uL3N1Ym1pc3Npb24vZ2xtbmV0XzAyLmNzdiIpDQpgYGANCg0KDQoNCg==